Skip to content

fix(tasks): don't flash the Slack icon on newly created tasks#2672

Merged
charlesvien merged 2 commits into
mainfrom
posthog-code/fix-new-task-slack-icon-flicker
Jun 16, 2026
Merged

fix(tasks): don't flash the Slack icon on newly created tasks#2672
charlesvien merged 2 commits into
mainfrom
posthog-code/fix-new-task-slack-icon-flicker

Conversation

@pauldambra

Copy link
Copy Markdown
Member

Problem

A newly created task in posthog/code would sometimes flash the Slack icon while it was still loading, even though it wasn't a Slack-triggered task. It self-corrected a moment later.

Root cause: useCreateTask.invalidateTasks optimistically inserts the new task into every task-list cache via setQueriesData({ queryKey: taskKeys.lists() }, ...), and taskKeys.lists() prefix-matches the slack-origin list too (useSlackTasks). Because /tasks/summaries/ omits origin_product, the sidebar infers a task's Slack origin purely by id membership in that slack list (buildSidebarData.ts). So the freshly created, non-slack task landed in slackTaskIds and rendered the Slack icon until useSlackTasks refetched from the server and dropped it.

Changes

Scope the optimistic insert to list caches that aren't filtered by origin_product, using a predicate that skips any list query whose filter has originProduct set. The slack-origin cache stays clean, so the new task never transiently shows the Slack icon.

How did you test this?

  • Added a regression test in useTaskCrudMutations.test.tsx asserting invalidateTasks seeds the plain list but not the slack-origin list. pnpm exec vitest run useTaskCrudMutations.test.tsx → 3 passed.
  • biome lint clean on both changed files.
  • pnpm --filter @posthog/ui typecheck passes.

Automatic notifications

  • Publish to changelog?
  • Alert Sales and Marketing teams?

Created with PostHog Code from a Slack thread

useCreateTask optimistically inserted the new task into every task-list
cache via setQueriesData on the taskKeys.lists() prefix, which also
matches the slack-origin list read by useSlackTasks. The sidebar derives
a task's origin (and Slack icon) by id membership in that list because
the summaries endpoint omits origin_product, so a freshly created,
non-slack task briefly rendered as a Slack task until the list refetched.

Scope the optimistic insert to lists that aren't filtered by
origin_product so the slack-origin cache stays clean.

Generated-By: PostHog Code
Task-Id: d2ed2d1b-2570-49a9-aa60-87e235a904f1
@github-actions

github-actions Bot commented Jun 15, 2026

Copy link
Copy Markdown

React Doctor found no issues in the changed files. 🎉

Reviewed by React Doctor for commit 6185233.

@greptile-apps

greptile-apps Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
packages/ui/src/features/tasks/useTaskCrudMutations.test.tsx:96-117
**Prefer parameterised tests**

The single `it` block verifies the slack case, but the predicate `!filters?.originProduct` is meant to exclude *any* origin-product-filtered cache, not just slack. A parameterised test would prove the fix holds for other origin products (e.g. `"linear"`, `"github"`) and that caches filtered only by non-`originProduct` fields (e.g. `{ repository: "my-repo" }`) are still optimistically seeded as intended. Testing only the slack example leaves the more general contract implicit.

Reviews (1): Last reviewed commit: "fix(tasks): don't seed the slack-origin ..." | Re-trigger Greptile

Comment on lines +96 to +117
it("seeds plain list caches but not the slack-origin list", () => {
const queryClient = new QueryClient();
const plainKey = taskKeys.list();
const slackKey = taskKeys.list({ originProduct: "slack" });
queryClient.setQueryData<Task[]>(plainKey, []);
queryClient.setQueryData<Task[]>(slackKey, []);

const localWrapper = ({ children }: { children: ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
const { result } = renderHook(() => useCreateTask(), {
wrapper: localWrapper,
});

result.current.invalidateTasks(createTask());

// The new, non-slack task lands in the plain list...
expect(queryClient.getQueryData<Task[]>(plainKey)).toHaveLength(1);
// ...but never pollutes the slack-origin list, which the sidebar reads to
// brand task icons by id membership.
expect(queryClient.getQueryData<Task[]>(slackKey)).toHaveLength(0);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Prefer parameterised tests

The single it block verifies the slack case, but the predicate !filters?.originProduct is meant to exclude any origin-product-filtered cache, not just slack. A parameterised test would prove the fix holds for other origin products (e.g. "linear", "github") and that caches filtered only by non-originProduct fields (e.g. { repository: "my-repo" }) are still optimistically seeded as intended. Testing only the slack example leaves the more general contract implicit.

Context Used: Do not attempt to comment on incorrect alphabetica... (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/ui/src/features/tasks/useTaskCrudMutations.test.tsx
Line: 96-117

Comment:
**Prefer parameterised tests**

The single `it` block verifies the slack case, but the predicate `!filters?.originProduct` is meant to exclude *any* origin-product-filtered cache, not just slack. A parameterised test would prove the fix holds for other origin products (e.g. `"linear"`, `"github"`) and that caches filtered only by non-`originProduct` fields (e.g. `{ repository: "my-repo" }`) are still optimistically seeded as intended. Testing only the slack example leaves the more general contract implicit.

**Context Used:** Do not attempt to comment on incorrect alphabetica... ([source](https://app.greptile.com/review/custom-context?memory=instruction-0))

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@pauldambra pauldambra marked this pull request as ready for review June 15, 2026 10:53

Copy link
Copy Markdown
Member Author

Note

🤖 Automated comment by QA Swarm — not written by a human

Multi-perspective review: paul-reviewer, xp-reviewer, security-audit (qa-team skipped — not present in this repo)

Verdict: 💬 APPROVE WITH NITS

Correct, well-targeted fix for a real, subtle bug. No correctness or security issues. One convergent design nit worth a look; the rest are optional.

Convergence (highest confidence — flagged independently by paul + xp)

  • useTaskCrudMutations.tsquery.queryKey[2] positional index + hand-written { originProduct?: string } type is fragile structural coupling. The key-shape knowledge (taskKeys.list(filters)[...lists(), filters]) lives in taskKeys, but this predicate re-encodes "filters are at slot 2" as a magic index plus a partial restatement of the real filters type. If taskKeys.list ever grows a slot ([...lists(), scope, filters]), the predicate silently matches the wrong slot and the Slack-icon bug quietly regresses — with no test failure. Suggestion: give taskKeys a typed accessor (e.g. taskKeys.filtersOf(queryKey)) so the shape knowledge stays OnceAndOnlyOnce beside where list() defines it. Ironic to guard one bit of positional/id coupling with another. (MEDIUM)

Nits (optional)

  • Predicate intent vs comment. Code rejects any origin-filtered list; the comment talks specifically about Slack. Broader is probably safer — just make the invariant the code protects and the comment agree. (LOW)
  • Name the rule, not the mechanism. return !filters?.originProduct; reads as "no origin product"; the actual rule is "origin-less lists mirror all new tasks." A named local (isOriginScopedList) or finishing the truncated comment would help. (LOW)
  • Test could assert the full contract. Good that both arms are covered (plain seeded → 1, slack not → 0). Worth also asserting the slack list is still invalidated (refetched), so a future refactor can't satisfy the length-0 expectation by dropping the slack list from refetch entirely. A natural it.each table, too. (LOW)

Reviewer summaries

Reviewer Assessment
👤 paul tidy fix, comment is genuinely good; only unease is guarding positional coupling with more positional coupling (queryKey[2]) — pull key-reading into taskKeys. not blocking.
📐 xp right shape (scope the seed, still invalidate all); both test arms covered. one theme: query-key structural knowledge duplicated in the consumer belongs with taskKeys. no must-fix.
🛡 security-audit no findings — client-side cache seeding of the user's own task into their own local cache, self-corrects on refetch; no trust boundary crossed.

Automated by QA Swarm — not a human review

Address qa-swarm review (paul + xp convergent): the create-task seed
predicate reached into the query key by positional index (queryKey[2])
and re-declared a partial filters type, duplicating key-shape knowledge
that belongs with taskKeys. A future key-shape change would silently
reintroduce the Slack-icon bug with no test failure.

- Add `TaskListFilters` type and a `taskKeys.filtersOf(queryKey)` accessor
  so the shape lives OnceAndOnlyOnce beside `list()`.
- Name the predicate's intent (`isOriginScopedList`) and finish the
  comment so code and intent agree (any origin-scoped list, not just slack).
- Parameterise the seed test across plain/repository/slack lists and add a
  test asserting all lists are still invalidated (refetched).

Generated-By: PostHog Code
Task-Id: d2ed2d1b-2570-49a9-aa60-87e235a904f1

Copy link
Copy Markdown
Member Author

Note

🤖 Automated comment by PR Shepherd — not written by a human

Actioned the qa-swarm review in 6185233:

  • Convergent MEDIUM (paul + xp): moved the list-key shape knowledge into taskKeys — added a TaskListFilters type and a taskKeys.filtersOf(queryKey) accessor, so the predicate no longer reaches in by positional [2] index or restates a partial filters type.
  • Nit (intent vs comment): named the rule isOriginScopedList and finished the comment so it reads "any origin-scoped list," matching the code.
  • Nit (test contract): parameterised the seed test across plain / repository / slack lists, and added a test asserting every list is still invalidated (refetched), so the no-seed expectation can't be "fixed" by dropping a list from refetch.

security-audit: no findings. All 6 tests pass, typecheck + biome clean.

@pauldambra pauldambra added the Stamphog This will request an autostamp by stamphog on small changes label Jun 15, 2026 — with PostHog

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low-risk UI cache fix: origin-scoped lists are excluded from optimistic seeding while still being invalidated for server reconciliation. The bot comment requesting parameterised tests is outdated — the current diff already uses it.each.

@pauldambra pauldambra requested a review from a team June 15, 2026 14:30
@charlesvien charlesvien merged commit 6004455 into main Jun 16, 2026
26 checks passed
@charlesvien charlesvien deleted the posthog-code/fix-new-task-slack-icon-flicker branch June 16, 2026 05:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Stamphog This will request an autostamp by stamphog on small changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants